---
title: nextjs
date: 2023-08-16 22:10:43
layout: PostBanner 
images: ['https://kodo.huixiangwuyou.com/blog/images/nextjs.jpeg']
summary: 'nextjs作为SEO服务端框架,可以很好的解决百度、谷歌SEO问题,并且可以很好的解决首屏加载问题,下面来简单介绍一下nextjs'
tags: 
 - web
 - next-js
---




## nextjs
Next.js 是一个基于 React 的开源框架，用于构建现代 Web 应用程序。它提供了服务器端渲染（SSR）、静态站点生成（SSG）、API 路由、增量静态再生（ISR）等功能，优化了性能、SEO 和开发体验。
详细介绍可以参考官网: [nextjs](https://nextjs.org/)

### nextjs的安装
```bash
npx create-next-app@latest

What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like your code inside a `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to use Turbopack for `next dev`?  No / Yes
Would you like to customize the import alias (`@/*` by default)? No / Yes
What import alias would you like configured? @/*

{
  "scripts": {
    "dev": "next dev", // 服务开发
    "build": "next build", // 构建打包项目
    "start": "next start", // 启动项目
  }
}
```

### nextjs的APP路由目录结构
```bash
├── .next
├── node_modules
├── app
│   ├── /components 组件
│   ├── /page1 其他跳转的页面文件夹作为路由
│   │   ├── /page3 
│   │   └── /page3 
│   ├── /page2 
│   │   ├── /page3 
│   │   └── /[id] 路径为 /page2/[id] 的页面
│   ├── /(page2)   用 （） 包裹表示这是某一个模块的路径，但是不读取括号内容作为路径一部分
│   │   ├── /@(id) 路径为 /page2/@(id) 的页面
│   │   └── /(..) photos\[id] 路径为 /page2/.. 的页面
│   ├── /dashboard 
│   │   ├── /[...slug]   表示 /dashboard/[...slug] 任意路由页面的动态路由，避免了dashboard路由下的404页面
│   │   └── /[id] 路径为 /page2/[id] 的页面
│   ├── layout.js
│   ├── about.js
│   └── index.js
├── public
│   └── favicon.ico
├── README.md
├── next.config.js
├── package.json
└── tsconfig.json
```

> layout.js 根布局文件,所有的页面都会被这个文件包裹。也可以在页面文件夹创建layout.js文件来包裹页面，页面的layout.js会覆盖根目录的layout.js

`layout.js` 是一个服务端组件不能使用 `use client` ,若是要使用请在页面或者组件中添加 `use client` 变成客户端 然后处理对应逻辑

```js
import type { Metadata } from "next";
import { Suspense } from 'react'
import localFont from "next/font/local";
import { NavigationEvents } from './components/navigation-events'
import Loading from './components/loading'
import "./globals.css";
// 设置网站字体
const geistSans = localFont({
  src: "./fonts/GeistVF.woff",
  variable: "--font-geist-sans",
  weight: "100 900",
});

const geistMono = localFont({
  src: "./fonts/GeistMonoVF.woff",
  variable: "--font-geist-mono",
  weight: "100 900",
});
// todo 添加meta标签
export const metadata: Metadata = {
  title: "Create Next website",
  description: "Generated by create next app",
  keywords:"first next demo"
};

// 或者动态设置标签
export async function generateMetadata({ params }) {
  return {
    title: '...',
  }
}

export default function RootLayout({
  children,
  team,
  analytics,
}: Readonly<{
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
}>) {
  return (
    // suppressHydrationWarning 去除警告 出现警告的原因可能是浏览器的插件冲突
    //  suppressHydrationWarning={true}
    <html lang="en">
      <meta name="description" content="next.js sss" />
      <title></title>
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
        <Suspense fallback={<Loading/>}>
          <NavigationEvents />
        </Suspense>
      </body>
    </html>
  );
}
```

### 渲染模式
| 渲染方式 | 适用场景 | SEO | 性能 | 开发复杂度 |
| --- | --- | --- | --- | --- |
| ​SSR | 动态内容（如电商、社交） | 高 | 中等（每次请求渲染） |  需处理数据获取 |
| SSG | 静态内容（如文档、博客） | 高 | 极快 | 需预构建 |
| ​ISR | 定期更新的内容 | 高 | 快 |  需配置 revalidate |
| ​CSR | 交互式应用（如仪表盘） | 低 |  依赖客户端 | 简单 |
|  |  |  |  |  |

#### SSR与SSG对比
| 特性 | SSR（服务端渲染） | CSR（客户端渲染） |
| --- | --- | --- |
| ​渲染位置 | 服务器 | 浏览器 |
| ​SEO 友好性 |  高（搜索引擎直接抓取 HTML） | 低（动态内容可能无法被爬取） |
| ​首屏加载速度 | 快（直接返回完整 HTML） |  慢（需等待 JS 加载） |
| ​服务器负载 | 高（每次请求都需渲染） | 低（初始 HTML 很小） |
| ​客户端交互 |  可能延迟（首次加载后仍需 JS）） | 流畅（数据动态更新） |
| ​适用场景 | 博客、新闻、电商商品页 |  仪表盘、社交应用、后台管理 |
|  |  |  |



### 路由方式
在项目中，app下面的每个文件夹就是路由的一个路径（用括号包裹作为模块的除外）

> 404 页面可以指定路由文件，或者直接在app目录下创建not-found.js文件，但是局部的404页面需要在页面`notFound()`函数指定自定义路由文件


#### 动态路由
在路由中，可以使用方括号`app/dashboard/[...slug]`来表示动态路由参数。例如，`/dashboard/[slug]`表示一个动态路由，其中`slug`是一个动态参数。当访问`/dashboard/page/a`时，`slug`的值为`page/a`。
也可以通过`app/page/[id]`方式定义动态路由,例如，`/page/[id]`表示一个动态路由，其中`id`是一个动态参数。当访问`/page/123`时，`id`的值为`123`。

#### 嵌套路由
在嵌套路由中，在父路由下的文件夹。这种情况下，父路由和子路由都可以独立地渲染和导航。例如商品列表和商品详情`product/list`和`product/detail`中，路由直接互不影响（当然管理后台类型的除外）

#### 平行路由
两个路由是平行的，有点类似嵌套路由，但是有区别的，平行路由是使您可以在同一布局中同时或有条件地渲染一个或多个页面。它们对于应用程序的高度动态部分，例如仪表板和社交网站上的供稿很有用。在切换的时候，父路由`dashboard`不会重新渲染，只有子路由会重新渲染（有操作过的数据在切换的时候会重写渲染）。
子路由在文件夹的展示为 `app/@analytics`或者`app/@team` ，通过在文件夹前面加上`@`来表示子路由分类文件夹，`文件夹下对应的文件夹名称就是对应子路由`，。然后插槽作为道具传递给共享在父页面引入展示
```js
├── app
│   ├── /@analytics 
│   │   ├── /visitor
│   │   │   └── page.js
│   │   └── default.js 默认展示
│   ├── /@team 
│   │   ├── /main
│   │   │   ├── error.js  可以单独处理错误页面
│   │   │   └── page.js
│   │   ├── /[routerName]   其他路由名称
│   │   │   └── page.js
│   │   └── default.js 默认展示
│   ├── layout.js
│   └── page.js
├── public


import type { Metadata } from "next";
import Link from "next/link";

// todo 添加meta标签
export const metadata: Metadata = {
  title: "parallel router page",
  description: "create next parallel",
  keywords:"create next parallel router"
};

export default function RootLayout({
  children,
  team,
  analytics,
}: Readonly<{
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
}>) {
  return (
    <div>
        <div className="max-w-min m-auto">
          <div className="flex justify-items-center justify-around">
            <Link href="/main" className="font-bold mr-[50px]">home</Link>
            <Link href="/visitor" className="font-bold">visitor</Link>
          </div>
          <div className="flex justify-items-center w-500">
            {team}
            {analytics}
          </div>
        </div>
        {children}
    </div>
  );
}
```
`team`和`analytics`组件都在插槽中，但是都是按条件渲染,默认条件下不渲染，在点击的时候会在`app/@analytics`或`app/@team`文件夹搜索对应的路由渲染，两个分类的文件夹可能也会出现相同的路由文件名称，那是对应路由的不同组件的展示，若是不变化，默认读取分类文件夹下的defualt.js文件

#### 拦截路由
在项目中经常会遇到有些页面需要登录才能访问，这时候就需要拦截路由，当用户未登录时，跳转到登录页面。在nextjs中，可以通过`app/page`目录下的`layout.js`文件来实现拦截路由。在`layout.js`文件中，可以通过`useUser`钩子获取当前用户的登录状态，如果用户未登录，则跳转到登录页面。

**图片详情预览示例**
``` js
// 路由路径
├── app
│   ├── /feed
│   │   ├── /@modal
│   │   │   │    └── /(..)photo
│   │   │   │    │   └──/[id]   
│   │   │   │    │      └── page.js
│   │   │   │    └── default.js
│   │   │   └── page.js
│   │   └── page.js 默认展示
│   ├── /photo 
│   │   ├── /[id]
│   │   │   └── page.js
│   │   └── page.js 
│   ├── layout.js
│   └── page.js
├── public

// 场景：在/feed路由下点击事件要预览/photo/[id]详情,但是/photo/[id]需要登录才能访问，这时候就需要拦截路由弹窗
// (..)photo/[id] 表示跳出当前一级目录，然后匹配photo/[id]路由
// (...)photo/[id]则表示在app目录下，然后匹配photo/[id]路由
// 在跳转 photo/[id] 路由前弹窗弹出一个modal页面，可以预览photo/[id]详情，而不需要刷新页面跳转。
// 若是刷新页面或者将链接分享给别人就跳转到了photo/[id]详情

// feed/page.js
"use client"
import Link from "next/link"
import Image from "next/image";

export default function Home() {
	return (
		<div>
			<div className='p-4 md:w-1/3'>
				<div className='h-full border-2 border-gray-200 border-opacity-60 rounded-lg overflow-hidden'>
      <Link href="/photos/55">
        <Image
         className='lg:h-48 md:h-36 w-full object-cover object-center'
         src='https://firebasestorage.googleapis.com/v0/b/thecaffeinecode.appspot.com/o/blog.jpg'
         alt='blog cover'
        />
      </Link>
				</div>
			</div>
		</div>
	)
}
```
> 登录拦截方式：在当前路由模块下创建_middleware.js
```js
// app/feed/_middleware.js
import { redirect } from "next/navigation";

export async function middleware(request) {
  const isLoggedIn = request.cookies.get("isLoggedIn");

  if (!isLoggedIn) {
    redirect("/login");
  }
}
```

### 数据请求
在`app/api`的文件夹处理各个页面的数据请求，通过`nextjs`的`fetch`方法来请求数据，然后返回给页面使用。在页面中，可以通过`useLoaderData`钩子来获取数据。
```js
├── app
│   ├── /api
│   │   ├── /articles
│   │   │   ├── /[id]  
│   │   │   │   └── route.js
│   │   │   └── route.js
│   │   └── / prodcut 
│   │       └── route.js
│   └── page.js
├── public

// route.js
import { NextResponse } from "next/server";

export async function GET(request) {
  const res = await fetch('https://api.example.com/articles')
  const data = await res.json()
  return NextResponse({
   code:200,
   data,
   revalidate: 60, // 60 秒后重新生成
  })
}
export async function Post(request) {
  console.log("🚀 ~ request:", request.url); // 获取请求路径信息
  console.log("🚀 ~ request:", request.headers); // 获取请求头信息
  const res = await fetch('https://api.example.com/articles')
  const data = await res.json()
  return NextResponse({
     date: new Date().toLocaleDateString(),
     time: new Date().toLocaleTimeString(),
  })
}

// 页面的请求路径就是 /api/articles 或者是 /api/articles/[id],请求的方法就是POST、GET
```

#### getServerSideProps|getProjects 

getServerSideProps api是在page路由模式在page.js页面设置，生命周期是在服务端请求数据
getProjects api是在app路由模式在page.js页面设置，生命周期是在服务端请求数据

​Next.js 13+：推荐使用 app 目录 + generateMetadata 或 generateViewport 替代 getServerSideProps
```js
export async function getServerSideProps() {
  const res = await fetch('https://api.example.com/data')
  const data = await res.json()
  return {
    props: {
      data
    }
  }
}
```
####  数据获取策略
| 方法 | 适用场景 | 数据获取时机 |
| --- | --- | --- |
| getServerSideProps | 动态数据（SSR） | 每次请求 |
| getStaticProps | 静态数据（SSG） | 构建时 |
| getStaticPaths | 动态路由（SSG） | 构建时 |
| getServerSideProps + revalidate | ISR | 构建后定期更新 |
|  |  |  |

#### 数据缓存
在`nextjs`中，可以通过`getStaticProps`和`getServerSideProps`来缓存数据，以减少服务器的请求次数，提高网站的加载速度。在`getStaticProps`和`getServerSideProps`中，可以通过`revalidate`参数来设置数据的缓存时间，单位是秒。例如，`revalidate: 60`表示数据缓存时间为60秒，即每60秒更新一次数据。

```js
// /api/../route.js
export const revalidate = 60;  //缓存数据时间60s 
export const dynamic = 'auto' | 'force-dynamic' | 'error' | 'force-static' // 设置获取的数据是否缓存

export async function Post(request) {
 return NextResponse({
   revalidate: 60,  //也可以在返回数据处理
 })
}
```

#### 数据处理
[lowdb](https://github.com/typicode/lowdb) 一个轻量级的本地 JSON 数据库,可以实现简单的客户端或服务器端数据存储

<br/>

#### getStaticProps
用于在 ​构建时 获取页面所需的数据，并将这些数据作为 props 传递给页面组件。 静态生成（SSG），适用于 SEO 和性能优化
适用场景：
- 数据不经常变化（如博客文章、产品列表）。
- 需要 SEO 优化的页面（搜索引擎可以抓取预渲染的 HTML）。
- 需要超快加载的页面（静态 HTML 比 SSR 更快）。
```js
export async function getStaticProps() {
  // 从 API 或数据库获取数据
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();
  // 返回数据作为 props
  return {
    props: { posts }, // 传递给页面组件
    revalidate: 60,   // 可选：60 秒后重新生成页面（ISR）
  };
}

export default function Posts({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}
```

#### getStaticPaths
getStaticPaths 用于 ​动态路由，告诉 Next.js 哪些路径需要预渲染（SSG）。
如果你的页面路径是动态的（如 /posts/[id]），必须使用 getStaticPaths 来指定哪些 [id] 需要预渲染。
```js
export async function getStaticPaths() {
  const res = await fetch('https://api.example.com/data')
  const data = await res.json()
  const paths = data.map((item) => ({
    params: { id: item.id.toString() },
  }))
  return {
    paths,
    fallback: false,
  }
}
```
### Middleware
中间件是一种在请求和响应之间执行的函数，可以用于处理请求、响应、路由等。在 Next.js 中，可以使用中间件来处理路由、请求、响应等。
`middleware.js 文件必须是在根目录下,否则可能不响应`
```js
import { NextResponse } from 'next/server'
 
export function middleware(request) {
  console.log("🚀 ~ middleware:", 1111);
  if(request.url.startsWith('/dashboard')) {
   return NextResponse.next()
  }
   // 如果不是登录页
  if(request.nextUrl.pathname !'/login'){
   // 并且没有 token
   const token = request.cookies.get("token')?.value
   if(!token){// 拦酸到登录页
    return NextResponse.redirect(new URL('/login', request.url))
   }
  }
  return NextResponse.redirect(new URL('/home', request.url))
}

export const config = {
 matcher: ['/dashboard', 
 '/feed/:path*', // 匹配 /feed 路由及其子路由
 '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
 ],  // 匹配的路由
}
```

### UI组件
nextjs内置了UI组件库，可以直接使用
- Image
- Link
- Head
- Meta
- Script
- Link
- Router

也可以使用官方推荐第三方UI组件库，[antd-design](https://ant-design.antgroup.com/docs/react/use-with-next-cn)、[templates](https://vercel.com/templates/next.js)、[creative-tim](https://www.creative-tim.com/templates/tailwind)

## 常用 API
### usePathname
获取当前路由的路径名

### useRouter
获取当前路由的信息

### useSearchParams
获取当前路由的查询参数

### useRouteLoaderData
获取当前路由的加载器数据

### useRouteError
获取当前路由的错误信息

### useSession
获取当前用户的会话信息

